Используйте один CancellationToken для заголовков и тела .NET HttpClient
Передавайте CancellationToken запроса и в подготовку ответа HttpClient, и в чтение тела, а не замеряйте только ожидание вызывающего кода.
Секундомер может прекратить ожидание, пока HTTP-запрос продолжается. Передавайте отмену самой операции, когда у попытки через прокси есть заданный бюджет времени.
Для современного приложения .NET с уже существующим HttpClient, настроенным на прокси:
using var budget = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var response = await client.GetAsync(
targetUrl,
HttpCompletionOption.ResponseHeadersRead,
budget.Token);
response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync(budget.Token);
Тот же бюджет начинается до подготовки и остаётся действующим при чтении тела. ResponseHeadersRead возвращает управление после заголовков, поэтому собственный тайм-аут HttpClient сам по себе не задаёт бюджет последующего чтения тела.
Держите отмену отдельно от HTTP-статуса и ошибок транспорта. OperationCanceledException может возникнуть из переданного токена или из другого настроенного тайм-аута; проверяйте соответствующий источник отмены, а не объявляйте любую ошибку мёртвым прокси.
Переиспользуйте клиент, которому принадлежит этот прокси-маршрут. Отмена одной попытки не означает пересборку всего пула соединений для каждого следующего запроса.
В примере буферизуется тело ответа конечного размера. При потоковой загрузке токен нужно передать в цикл копирования потока и освобождать ответ на каждом пути выхода.
Отмена выражает политику операции, а не точное обещание времени для каждого действия платформы. Не заменяйте её бесконечным циклом опроса Task.IsCompleted.