Repository: encode/httpx (Python)

Issue:
Fine-tuning timeouts from the default timeout config
Currently, if we want to change only one of the three timeouts (connect, read, write) we need to build a full-blown `TimeoutConfig`. But there is no way to use the default timeout values.

```python
>>> import httpx
>>> httpx.config.DEFAULT_TIMEOUT_CONFIG
TimeoutConfig(timeout=5.0)
>>> httpx.TimeoutConfig(read_timeout=1)
# We'd expect connect_timeout and write_timeout to be 5 (the default)
TimeoutConfig(connect_timeout=None, read_timeout=1, write_timeout=None)
```

Changing `TimeoutConfig` to automatically use the default values in case some values aren't given is probably not the good way to go, because `TimeoutConfig` shouldn't contain any smart behavior.

Instead, we should probably allow explicitly building a `TimeoutConfig` instance from `DEFAULT_TIMEOUT_CONFIG`:

I'd see it materialize as factory method on `TimeoutConfig`:

```python
>>> httpx.TimeoutConfig.from_defaults(read_timeout=1)
TimeoutConfig(connect_timeout=5, read_timeout=1, write_timeout=5)
```

Does this sound reasonable @tomchristie?
TimeoutConfig improvements.
1. `TimeoutConfig(5.0)` should really just be `Timeout(5.0)`. We don't want to break the API prior to 1.0 so I guess let's switch it to `Timeout` but have a `TimeoutConfig = Timeout` synonym.

2. Right now we either support `Timeout(5.0)` or `Timeout(read_timeout=5.0)`, however we should actually support a style that provides the default case, and the exceptional cases. Eg...

```python
# Connect, Read, Write have a 5 second timeout. Pool acquiry never times out.
timeout = httpx.Timeout(5.0, pool_timeout=None)
client = Client(timeout=timeout)
```


Discussion:
What about making `TimeoutConfig(read_timeout=1)` result in 5, 1, 5 instead of None, 1, None? Then you have to be explicit about the other two if you truly want "None".
Simplest question is, what do we want this to return?...

```python
>>> httpx.TimeoutConfig()
```

I *think* we should probably switch things around so that it'll give you `TimeoutConfig(timeout=5)` rather than `TimeoutConfig(timeout=None)`, right?

I guess we want something like this...


```python
DEFAULT_TIMEOUT = 5
UNSET = object()

class TimeoutConfig:
    def __init__(
        self,
        timeout: TimeoutTypes = DEFAULT_TIMEOUT,
        *,
        connect_timeout: float = UNSET,
        read_timeout: float = UNSET,
        write_timeout: float = UNSET,
    ):
        self.connect_timeout = timeout if connect_timeout is UNSET else connect_timeout
        self.read_timeout = timeout if read_timeout is UNSET else read_timeout
        self.write_timeout = timeout if connect_timeout is UNSET else write_timeout
        ...
```
And so we'd have:

```python
>>> # Override one of the timeouts.
>>> httpx.TimeoutConfig(read_timeout=1)
TimeoutConfig(connect_timeout=5, read_timeout=1, write_timeout=5)
>>> # Disable timeouts.
>>> httpx.TimeoutConfig(timeout=None)
TimeoutConfig(connect_timeout=None, read_timeout=None, write_timeout=None)
>>> # Set all timeouts to 3 seconds but disable read.
>>> httpx.TimeoutConfig(timeout=3, read_timeout=None)
TimeoutConfig(connect_timeout=3, read_timeout=None, write_timeout=3)
```

This looks like a nice approach to me. I suppose we'd get rid of `DEFAULT_TIMEOUT_CONFIG` entirely and use `TimeoutConfig()` instead?





Patch:
diff --git a/docs/advanced.md b/docs/advanced.md
index e13ea452f4..42346d5039 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -250,83 +250,78 @@ async with httpx.Client(proxies=proxy) as client:
     has not been implemented yet. To use proxies you must pass the proxy
     information at `Client` initialization.
 
-## Timeout fine-tuning
+## Timeout Configuration
 
-HTTPX offers various request timeout management options. Three types of timeouts
-are available: **connect** timeouts, **write** timeouts and **read** timeouts.
+HTTPX is careful to enforce timeouts everywhere by default.
 
-* The **connect timeout** specifies the maximum amount of time to wait until
-a connection to the requested host is established. If HTTPX is unable to connect
-within this time frame, a `ConnectTimeout` exception is raised.
-* The **write timeout** specifies the maximum duration to wait for a chunk of
-data to be sent (for example, a chunk of the request body). If HTTPX is unable
-to send data within this time frame, a `WriteTimeout` exception is raised.
-* The **read timeout** specifies the maximum duration to wait for a chunk of
-data to be received (for example, a chunk of the response body). If HTTPX is
-unable to receive data within this time frame, a `ReadTimeout` exception is raised.
-
-### Setting timeouts
+The default behavior is to raise a `TimeoutException` after 5 seconds of
+network inactivity.
 
-You can set timeouts on two levels:
+### Setting and disabling timeouts
 
-- For a given request:
+You can set timeouts for an individual request:
 
 ```python
-# Using top-level API
-await httpx.get('http://example.com/api/v1/example', timeout=5)
+# Using the top-level API:
+await httpx.get('http://example.com/api/v1/example', timeout=10.0)
 
-# Or, with a client:
+# Using a client instance:
 async with httpx.Client() as client:
-    await client.get("http://example.com/api/v1/example", timeout=5)
+    await client.get("http://example.com/api/v1/example", timeout=10.0)
 ```
 
-- On a client instance, which results in the given `timeout` being used as a default for requests made with this client:
+Or disable timeouts for an individual request:
 
 ```python
-async with httpx.Client(timeout=5) as client:
-    await client.get('http://example.com/api/v1/example')
+# Using the top-level API:
+await httpx.get('http://example.com/api/v1/example', timeout=None)
+
+# Using a client instance:
+async with httpx.Client() as client:
+    await client.get("http://example.com/api/v1/example", timeout=None)
 ```
 
-Besides, you can pass timeouts in two forms:
+### Setting a default timeout on a client
 
-- A number, which sets the read, write and connect timeouts to the same value, as in the examples above.
-- A `Timeout` instance, which allows to define the read, write and connect timeouts independently:
+You can set a timeout on a client instance, which results in the given
+`timeout` being used as the default for requests made with this client:
 
 ```python
-timeout = httpx.Timeout(
-    connect_timeout=5,
-    read_timeout=10,
-    write_timeout=15
-)
-
-resp = await httpx.get('http://example.com/api/v1/example', timeout=timeout)
+client = httpx.Client()              # Use a default 5s timeout everywhere.
+client = httpx.Client(timeout=10.0)  # Use a default 10s timeout everywhere.
+client = httpx.Client(timeout=None)  # Disable all timeouts by default.
 ```
 
-### Default timeouts
-
-By default all types of timeouts are set to 5 second.
+### Fine tuning the configuration
 
-### Disabling timeouts
+HTTPX also allows you to specify the timeout behavior in more fine grained detail.
 
-To disable timeouts, you can pass `None` as a timeout parameter.
-Note that currently this is not supported by the top-level API.
-
-```python
-url = "http://example.com/api/v1/delay/10"
-
-await httpx.get(url, timeout=None)  # Times out after 5s
+There are four different types of timeouts that may occur. These are **connect**,
+**read**, **write**, and **pool** timeouts.
 
+* The **connect timeout** specifies the maximum amount of time to wait until
+a connection to the requested host is established. If HTTPX is unable to connect
+within this time frame, a `ConnectTimeout` exception is raised.
+* The **read timeout** specifies the maximum duration to wait for a chunk of
+data to be received (for example, a chunk of the response body). If HTTPX is
+unable to receive data within this time frame, a `ReadTimeout` exception is raised.
+* The **write timeout** specifies the maximum duration to wait for a chunk of
+data to be sent (for example, a chunk of the request body). If HTTPX is unable
+to send data within this time frame, a `WriteTimeout` exception is raised.
+* The **pool timeout** specifies the maximum duration to wait for acquiring
+a connection from the connection pool. If HTTPX is unable to acquire a connection
+within this time frame, a `PoolTimeout` exception is raised. A related
+configuration here is the maximum number of allowable connections in the
+connection pool, which is configured by the `pool_limits`.
 
-async with httpx.Client(timeout=None) as client:
-    await client.get(url)  # Does not timeout, returns after 10s
+You can configure the timeout behavior for any of these values...
 
+```python
+# A client with a 60s timeout for connecting, and a 10s timeout elsewhere.
+timeout = httpx.Timeout(10.0, connect_timeout=60.0)
+client = httpx.Client(timeout=timeout)
 
-timeout = httpx.Timeout(
-    connect_timeout=5,
-    read_timeout=None,
-    write_timeout=5
-)
-await httpx.get(url, timeout=timeout) # Does not timeout, returns after 10s
+response = await client.get('http://example.com/')
 ```
 
 ## Multipart file encoding
diff --git a/docs/quickstart.md b/docs/quickstart.md
index f1b9be8391..636dfe6bb0 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -381,6 +381,12 @@ value to be more or less strict:
 >>> await httpx.get('https://github.com/', timeout=0.001)
 ```
 
+You can also disable the timeout behavior completely...
+
+```python
+>>> await httpx.get('https://github.com/', timeout=None)
+```
+
 For advanced timeout management, see [Timeout fine-tuning](https://www.encode.io/httpx/advanced/#timeout-fine-tuning).
 
 ## Authentication
diff --git a/httpx/__init__.py b/httpx/__init__.py
index 69f7a42142..febe2c708b 100644
--- a/httpx/__init__.py
+++ b/httpx/__init__.py
@@ -36,10 +36,10 @@
     ReadTimeout,
     RedirectBodyUnavailable,
     RedirectLoop,
-    RequestTimeout,
     ResponseClosed,
     ResponseNotRead,
     StreamConsumed,
+    TimeoutException,
     TooManyRedirects,
     WriteTimeout,
 )
@@ -127,7 +127,7 @@
     "QueryParamTypes",
     "Request",
     "RequestData",
-    "RequestTimeout",
+    "TimeoutException",
     "Response",
     "ResponseContent",
     "RequestFiles",
diff --git a/httpx/client.py b/httpx/client.py
index 36f6a2404d..24b6a457dd 100644
--- a/httpx/client.py
+++ b/httpx/client.py
@@ -10,10 +10,12 @@
     DEFAULT_MAX_REDIRECTS,
     DEFAULT_POOL_LIMITS,
     DEFAULT_TIMEOUT_CONFIG,
+    UNSET,
     CertTypes,
     PoolLimits,
     Timeout,
     TimeoutTypes,
+    UnsetType,
     VerifyTypes,
 )
 from .dispatch.asgi import ASGIDispatch
@@ -50,13 +52,6 @@
 logger = get_logger(__name__)
 
 
-class UnsetType:
-    pass  # pragma: nocover
-
-
-UNSET = UnsetType()
-
-
 class Client:
     """
     An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
diff --git a/httpx/config.py b/httpx/config.py
index 1093441549..34c3ad04f5 100644
--- a/httpx/config.py
+++ b/httpx/config.py
@@ -38,6 +38,13 @@
 logger = get_logger(__name__)
 
 
+class UnsetType:
+    pass  # pragma: nocover
+
+
+UNSET = UnsetType()
+
+
 class SSLConfig:
     """
     SSL Configuration.
@@ -203,44 +210,58 @@ def _load_client_certs(self, ssl_context: ssl.SSLContext) -> None:
 
 class Timeout:
     """
-    Timeout values.
+    Timeout configuration.
+
+    **Usage**:
+
+    Timeout()                           # No timeout.
+    Timeout(5.0)                        # 5s timeout on all operations.
+    Timeout(connect_timeout=5.0)        # 5s timeout on connect, no other timeouts.
+    Timeout(5.0, connect_timeout=10.0)  # 10s timeout on connect. 5s timeout elsewhere.
+    Timeout(5.0, pool_timeout=None)     # No timeout on acquiring connection from pool.
+                                        # 5s timeout elsewhere.
     """
 
     def __init__(
         self,
         timeout: TimeoutTypes = None,
         *,
-        connect_timeout: float = None,
-        read_timeout: float = None,
-        write_timeout: float = None,
-        pool_timeout: float = None,
+        connect_timeout: typing.Union[None, float, UnsetType] = UNSET,
+        read_timeout: typing.Union[None, float, UnsetType] = UNSET,
+        write_timeout: typing.Union[None, float, UnsetType] = UNSET,
+        pool_timeout: typing.Union[None, float, UnsetType] = UNSET,
     ):
-        if timeout is None:
-            self.connect_timeout = connect_timeout
-            self.read_timeout = read_timeout
-            self.write_timeout = write_timeout
-            self.pool_timeout = pool_timeout
+        if isinstance(timeout, Timeout):
+            # Passed as a single explicit Timeout.
+            assert connect_timeout is UNSET
+            assert read_timeout is UNSET
+            assert write_timeout is UNSET
+            assert pool_timeout is UNSET
+            self.connect_timeout = (
+                timeout.connect_timeout
+            )  # type: typing.Optional[float]
+            self.read_timeout = timeout.read_timeout  # type: typing.Optional[float]
+            self.write_timeout = timeout.write_timeout  # type: typing.Optional[float]
+            self.pool_timeout = timeout.pool_timeout  # type: typing.Optional[float]
+        elif isinstance(timeout, tuple):
+            # Passed as a tuple.
+            self.connect_timeout = timeout[0]
+            self.read_timeout = timeout[1]
+            self.write_timeout = None if len(timeout) < 3 else timeout[2]
+            self.pool_timeout = None if len(timeout) < 4 else timeout[3]
         else:
-            # Specified as a single timeout value
-            assert connect_timeout is None
-            assert read_timeout is None
-            assert write_timeout is None
-            assert pool_timeout is None
-            if isinstance(timeout, Timeout):
-                self.connect_timeout = timeout.connect_timeout
-                self.read_timeout = timeout.read_timeout
-                self.write_timeout = timeout.write_timeout
-                self.pool_timeout = timeout.pool_timeout
-            elif isinstance(timeout, tuple):
-                self.connect_timeout = timeout[0]
-                self.read_timeout = timeout[1]
-                self.write_timeout = None if len(timeout) < 3 else timeout[2]
-                self.pool_timeout = None if len(timeout) < 4 else timeout[3]
-            else:
-                self.connect_timeout = timeout
-                self.read_timeout = timeout
-                self.write_timeout = timeout
-                self.pool_timeout = timeout
+            self.connect_timeout = (
+                timeout if isinstance(connect_timeout, UnsetType) else connect_timeout
+            )
+            self.read_timeout = (
+                timeout if isinstance(read_timeout, UnsetType) else read_timeout
+            )
+            self.write_timeout = (
+                timeout if isinstance(write_timeout, UnsetType) else write_timeout
+            )
+            self.pool_timeout = (
+                timeout if isinstance(pool_timeout, UnsetType) else pool_timeout
+            )
 
     def __eq__(self, other: typing.Any) -> bool:
         return (
diff --git a/httpx/exceptions.py b/httpx/exceptions.py
index 61710cf531..b19669c6ab 100644
--- a/httpx/exceptions.py
+++ b/httpx/exceptions.py
@@ -20,31 +20,31 @@ def __init__(
 # Timeout exceptions...
 
 
-class RequestTimeout(HTTPError):
+class TimeoutException(HTTPError):
     """
     A base class for all timeouts.
     """
 
 
-class ConnectTimeout(RequestTimeout):
+class ConnectTimeout(TimeoutException):
     """
     Timeout while establishing a connection.
     """
 
 
-class ReadTimeout(RequestTimeout):
+class ReadTimeout(TimeoutException):
     """
     Timeout while reading response data.
     """
 
 
-class WriteTimeout(RequestTimeout):
+class WriteTimeout(TimeoutException):
     """
     Timeout while writing request data.
     """
 
 
-class PoolTimeout(RequestTimeout):
+class PoolTimeout(TimeoutException):
     """
     Timeout while waiting to acquire a connection from the pool.
     """


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 0109e8f3e3..ad7a626cae 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -156,6 +156,16 @@ def test_timeout_from_one_none_value():
     assert timeout == httpx.Timeout()
 
 
+def test_timeout_from_one_value():
+    timeout = httpx.Timeout(read_timeout=5.0)
+    assert timeout == httpx.Timeout(timeout=(None, 5.0, None, None))
+
+
+def test_timeout_from_one_value_and_default():
+    timeout = httpx.Timeout(5.0, pool_timeout=60.0)
+    assert timeout == httpx.Timeout(timeout=(5.0, 5.0, 5.0, 60.0))
+
+
 def test_timeout_from_tuple():
     timeout = httpx.Timeout(timeout=(5.0, 5.0, 5.0, 5.0))
     assert timeout == httpx.Timeout(timeout=5.0)