Repository: encode/httpx

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?

Discussion:

Patch:
diff --git a/README.md b/README.md
index 808e77b..358e98a 100644
--- a/README.md
+++ b/README.md
@@ -24,12 +24,12 @@ or trio, and is able to support making large numbers of requests concurrently.
 **Note**: *HTTPX should still be considered in alpha. We'd love early users and feedback,
 but would strongly recommend pinning your dependencies to the latest median
 release, so that you're able to properly review API changes between package
-updates. Currently you should be using `http==0.8.*`.*
+updates. Currently you should be using `httpx==0.8.*`.*
 
 *In particular, the 0.8 release switched HTTPX into focusing exclusively on
 providing an async client, in order to move the project forward, and help
 us [change our approach to providing sync+async support][sync-support]. If
-you have been using the sync client, you may want to pin to `http==0.7.*`,
+you have been using the sync client, you may want to pin to `httpx==0.7.*`,
 and wait until our sync client is reintroduced.*
 
 ---
diff --git a/docs/advanced.md b/docs/advanced.md
index e13ea45..42346d5 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/index.md b/docs/index.md
index 746ed59..6fd73f3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -33,12 +33,12 @@ or trio, and is able to support making large numbers of concurrent requests.
     HTTPX should currently be considered in alpha. We'd love early users and feedback,
     but would strongly recommend pinning your dependencies to the latest median
     release, so that you're able to properly review API changes between package
-    updates. Currently you should be using `http==0.8.*`.
+    updates. Currently you should be using `httpx==0.8.*`.
 
     In particular, the 0.8 release switched HTTPX into focusing exclusively on
     providing an async client, in order to move the project forward, and help
     us [change our approach to providing sync+async support][sync-support]. If
-    you have been using the sync client, you may want to pin to `http==0.7.*`,
+    you have been using the sync client, you may want to pin to `httpx==0.7.*`,
     and wait until our sync client is reintroduced.
 
 ---
diff --git a/docs/quickstart.md b/docs/quickstart.md
index f1b9be8..636dfe6 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 69f7a42..febe2c7 100644
--- a/httpx/__init__.py
+++ b/httpx/__init__.py
@@ -36,10 +36,10 @@ from .exceptions import (
     ReadTimeout,
     RedirectBodyUnavailable,
     RedirectLoop,
-    RequestTimeout,
     ResponseClosed,
     ResponseNotRead,
     StreamConsumed,
+    TimeoutException,
     TooManyRedirects,
     WriteTimeout,
 )
@@ -127,7 +127,7 @@ __all__ = [
     "QueryParamTypes",
     "Request",
     "RequestData",
-    "RequestTimeout",
+    "TimeoutException",
     "Response",
     "ResponseContent",
     "RequestFiles",
diff --git a/httpx/client.py b/httpx/client.py
index 36f6a24..24b6a45 100644
--- a/httpx/client.py
+++ b/httpx/client.py
@@ -10,10 +10,12 @@ from .config import (
     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 @@ from .utils import ElapsedTimer, NetRCInfo, get_environment_proxies, get_logger
 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 1093441..34c3ad0 100644
--- a/httpx/config.py
+++ b/httpx/config.py
@@ -38,6 +38,13 @@ DEFAULT_CIPHERS = ":".join(
 logger = get_logger(__name__)
 
 
+class UnsetType:
+    pass  # pragma: nocover
+
+
+UNSET = UnsetType()
+
+
 class SSLConfig:
     """
     SSL Configuration.
@@ -203,44 +210,58 @@ class SSLConfig:
 
 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 61710cf..b19669c 100644
--- a/httpx/exceptions.py
+++ b/httpx/exceptions.py
@@ -20,31 +20,31 @@ class HTTPError(Exception):
 # 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 0109e8f..ad7a626 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)